Skip to content

Return false from == and nil from <=> for incomparable objects#31

Merged
nertzy merged 1 commit into
minad:masterfrom
nertzy:comparison-never-raises
Jun 12, 2026
Merged

Return false from == and nil from <=> for incomparable objects#31
nertzy merged 1 commit into
minad:masterfrom
nertzy:comparison-never-raises

Conversation

@nertzy

@nertzy nertzy commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Unit subclasses Numeric, so == and <=> routed every non-Numeric operand through #apply_through_coercion, which re-raises as TypeError whenever the operand doesn't implement #coerce. Ordinary expressions like Unit(1) == nil raised instead of returning false, breaking Ruby's contract that == never raises and that <=> returns nil for incomparable operands.

Guard the coercion path with respond_to?(:coerce): objects that opt into the coercion protocol (e.g. the UnitOne spec helper) still flow through it, while plain objects short-circuit — == returns false and <=> returns nil. Comparable then raises ArgumentError for ordered comparisons like Unit(1) > nil, matching core Numeric behavior.

Expression Before After
Unit(1) == nil TypeError false
Unit(1) != nil TypeError true
Unit(1) <=> nil TypeError nil
Unit(1) > nil TypeError ArgumentError
[Unit(1)].include?(nil) TypeError false

Unit subclasses Numeric, so == and <=> routed every non-Numeric
operand through #apply_through_coercion, which re-raises as TypeError
whenever the operand does not implement #coerce. That made ordinary
expressions like `Unit(1) == nil` raise instead of returning false,
breaking Ruby's contract that == never raises and that <=> returns nil
for incomparable operands.

Guard the coercion path with respond_to?(:coerce) so objects that opt
into the coercion protocol (e.g. the UnitOne spec helper) still flow
through it, while plain objects short-circuit: == returns false and
<=> returns nil. Comparable then raises ArgumentError for ordered
comparisons such as `Unit(1) > nil`, matching core Numeric behavior.
@nertzy nertzy merged commit bf16c4b into minad:master Jun 12, 2026
4 checks passed
nertzy added a commit to nertzy/unit that referenced this pull request Jun 12, 2026
Following minad#31, <=> returned nil for non-coercible objects, but the
Numeric branch still raised IncompatibleUnitError (a TypeError) whenever
two units had incompatible dimensions. Routed through Comparable that
surfaced as the unhelpful "comparison of Unit with Unit failed", and
sort, min, max, between? and clamp blew up with a non-standard error.

Raise ArgumentError directly from <=> when the operands are dimensionally
incompatible. Every Comparable path funnels through <=>, so the same
descriptive message now surfaces from <, >, sort, min, max, between? and
clamp. A sibling Unit is shown via #inspect (Unit("1 s")); any other
numeric is shown by class (Float, Integer) rather than its coerced unit
or a potentially large #inspect, matching core Ruby's own phrasing.

Comparing against a genuinely foreign object (nil, String) still returns
nil, matching 1 <=> "a"; only dimension mismatches between quantities
raise. Arithmetic (+/-) keeps raising IncompatibleUnitError.
nertzy added a commit to nertzy/unit that referenced this pull request Jun 12, 2026
Following minad#31, <=> returned nil for non-coercible objects, but the
Numeric branch still raised IncompatibleUnitError (a TypeError) whenever
two units had incompatible dimensions. Routed through Comparable that
surfaced as the unhelpful "comparison of Unit with Unit failed", and
sort, min, max, between? and clamp blew up with a non-standard error.

Raise ArgumentError directly from <=> when the operands are dimensionally
incompatible. Every Comparable path funnels through <=>, so the same
descriptive message now surfaces from <, >, sort, min, max, between? and
clamp. A sibling Unit is shown via #inspect (Unit("1 s")); any other
numeric is shown by class (Float, Integer) rather than its coerced unit
or a potentially large #inspect, matching core Ruby's own phrasing.

Comparing against a genuinely foreign object (nil, String) still returns
nil, matching 1 <=> "a"; only dimension mismatches between quantities
raise. Arithmetic (+/-) keeps raising IncompatibleUnitError.
@nertzy nertzy deleted the comparison-never-raises branch June 15, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant